internal: add HTTProxy CORS support#2890
Conversation
Codecov Report
@@ Coverage Diff @@
## main #2890 +/- ##
==========================================
+ Coverage 74.91% 75.18% +0.26%
==========================================
Files 87 87
Lines 5604 5677 +73
==========================================
+ Hits 4198 4268 +70
- Misses 1315 1316 +1
- Partials 91 93 +2
|
31f3da8 to
97f6d70
Compare
skriss
left a comment
There was a problem hiding this comment.
Thanks for the PR @aberasarte! I added a few initial comments.
internal/dag/httpproxy_processor.go
Outdated
| maxAge := timeout.DefaultSetting() | ||
| if perTryMaxAge, err := time.ParseDuration(policy.MaxAge); err == nil { | ||
| maxAge = timeout.DurationSetting(perTryMaxAge) | ||
| if maxAge.Duration().Seconds() < 0 { | ||
| return nil, fmt.Errorf("invalid max age value: %q", policy.MaxAge) | ||
| } | ||
| } |
There was a problem hiding this comment.
Do you want to support disabling this setting? Per https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age, the value -1 disables the timeout.
If so, the standard for other Contour timeouts is that the input string infinity disables the timeout. The timeout.Parse function already handles parsing this.
There was a problem hiding this comment.
In most other contexts "disables the timeout" means that the operation can (in principle) take forever, but -1 here means that the max age never applies, so max-age of "infinity" seems misleading. For practical purposes, a max-age of -1 and 0 seem like they would be treated similarly?
There was a problem hiding this comment.
For practical purposes, a max-age of -1 and 0 seem like they would be treated similarly?
Both 0 and -1 mean disable caching for the preflight request but I decided to support just the 0 value. I found tricky to handle the -1 taking into account that the durations are expressed in the Go duration format. If we wanted to support -1 how would we do that? -1s disables caching but -1h is an invalid value? I'd rather allow negative values and assume that all of them disable the cache.
internal/envoy/route.go
Outdated
| if !cp.MaxAge.UseDefault() { | ||
| rcp.MaxAge = fmt.Sprintf("%.0f", cp.MaxAge.Duration().Seconds()) | ||
| } |
There was a problem hiding this comment.
If you want to support disabling MaxAge (see other comment), then you'd also want to check for cp.MaxAge.IsDisabled() here, and if true, pass a value of -1 to Envoy.
There was a problem hiding this comment.
yeh, -1 here means an immediate timeout, not no timeout.
There was a problem hiding this comment.
Now I'm doing that but for the 0 value.
jpeach
left a comment
There was a problem hiding this comment.
This looks like it's in pretty good shape to me. I have a few comments. I'll be out of office for a couple of weeks, so happy to defer to @skriss on reviewing subsequent iterations.
Thanks @aberasarte :)
apis/projectcontour/v1/httpproxy.go
Outdated
| ExposeHeaders []string `json:"exposeHeaders"` | ||
| // +optional | ||
| // MaxAge specifies the content for the *access-control-max-age* header. | ||
| MaxAge string `json:"maxAge"` |
There was a problem hiding this comment.
Since MaxAge is parsed by timeout.Setting, please include the boilerplate format description (this will be consolidated into validation regex in the future):
// MaAge durations are expressed in the Go [Duration format](https://godoc.org/time#ParseDuration).
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
// The string "infinity" is also a valid input and specifies no timeout.
// A value of "0s" will be treated as if the field were not set, i.e. by using Envoy's default behavior.
apis/projectcontour/v1/httpproxy.go
Outdated
| AllowMethods []string `json:"allowMethods"` | ||
| // AllowHeaders specifies the content for the *access-control-allow-headers* header. | ||
| // +optional | ||
| AllowHeaders []string `json:"allowHeaders"` |
There was a problem hiding this comment.
These strings end up being joined with , and passed directly to one envoy filter, so we should add validation. Here's what I suggest (the pattern approximates the tchar spec from RFC 7230 sec 3.2.6):
// +kubebuilder:validation:Pattern=[a-zA-Z0-9!#$%&'*+.^_`|~-]+
type CorsHeaderValue string
type CorsPolicy struct {
AllowOrigin []CORSHeaderValue
AllowMethods []CORSHeaderValue
AllowHeaders []CORSHeaderValue
ExposeHeaders []CORSHeaderValue
}
```There was a problem hiding this comment.
We can't apply this validation pattern for the AllowOrigin header as URLs like http://example.com are valid values. @skriss should I apply it for the rest of the headers and leave AllowOrigin as a []string?
internal/dag/httpproxy_processor.go
Outdated
| maxAge := timeout.DefaultSetting() | ||
| if perTryMaxAge, err := time.ParseDuration(policy.MaxAge); err == nil { | ||
| maxAge = timeout.DurationSetting(perTryMaxAge) | ||
| if maxAge.Duration().Seconds() < 0 { | ||
| return nil, fmt.Errorf("invalid max age value: %q", policy.MaxAge) | ||
| } | ||
| } |
There was a problem hiding this comment.
In most other contexts "disables the timeout" means that the operation can (in principle) take forever, but -1 here means that the max age never applies, so max-age of "infinity" seems misleading. For practical purposes, a max-age of -1 and 0 seem like they would be treated similarly?
| }, | ||
| &http.HttpFilter{ | ||
| Name: wellknown.CORS, | ||
| }, |
There was a problem hiding this comment.
Reading the envoy docs, it's not clear to me whether simply adding the filter activates CORS. The filter_enabled field says:
If neither enabled, filter_enabled, nor shadow_enabled are specified, the CORS filter will be enabled for 100% of the requests.
So does that mean that when CORS policy is omitted, we need to generate a CorsPolicy_Enabled message with Enabled=false?
There was a problem hiding this comment.
After testing it, I think that if we don't set the EnabledSpecifier property of &envoy_api_v2_route.CorsPolicy, the CORS filter will be enabled by default. When CORS configuration is omitted, we are not setting any CORS policy to the VirtualHost therefore, the filter is disabled.
internal/envoy/route.go
Outdated
| Numerator: 100, | ||
| Denominator: envoy_type.FractionalPercent_HUNDRED, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Rather than enable this a fractional 100%, would it be simpler to do:
EnabledSpecified: &CorsPolicy_Enabled {
Enabled: protobuf.Bool(true),
}There was a problem hiding this comment.
I think that we should keep using filter_enabled as enabled only applies to routes and according to the docs, it is deprecated:
enabled
(BoolValue) Specifies if the CORS filter is enabled. Defaults to true. Only effective on route.
Attention
This field is deprecated. Set the filter_enabled field instead.
There was a problem hiding this comment.
I've ended up leaving the EnabledSpecified property unset as the CORS filter is enabled by default (see my comment above).
internal/envoy/route.go
Outdated
| if !cp.MaxAge.UseDefault() { | ||
| rcp.MaxAge = fmt.Sprintf("%.0f", cp.MaxAge.Duration().Seconds()) | ||
| } |
There was a problem hiding this comment.
yeh, -1 here means an immediate timeout, not no timeout.
b7398cc to
3f23600
Compare
|
I think that all the comments have been addressed so it should be ready for a second review, PTAL @skriss . If everything looks good, I'll rebase the |
|
@aberasarte I'll take another look through this afternoon; thanks for the updates! |
skriss
left a comment
There was a problem hiding this comment.
@aberasarte this is looking pretty good, I just had a few more comments. Feel free to rebase when you're ready.
I am taking one more look at the MaxAge field just to make sure we have reasonable/consistent API semantics, I'll add another comment on that shortly.
internal/envoy/route.go
Outdated
| } | ||
|
|
||
| rcp.AllowOriginStringMatch = []*matcher.StringMatcher{} | ||
| for _, ao := range cp.AllowOrigin { |
There was a problem hiding this comment.
It's worth a comment here about that since it's non-obvious.
|
@aberasarte, this seems pretty close! If you can get this merged in the next 7 days, we should be able to get it into Contour 1.9. Seems like it just needs a rebase and the MaxAge fix? @skriss is that the only todo left? |
|
I had a few other comments that still need to be addressed, but it was nothing major. Agree that we should be able to get this into 1.9! |
793a591 to
64824db
Compare
|
All comments addressed and |
skriss
left a comment
There was a problem hiding this comment.
Just one minor thing, plus it looks like one more rebase is needed (sorry, we've been merging a lot of big changes lately).
c602dd0 to
9249b7b
Compare
skriss
left a comment
There was a problem hiding this comment.
No further comments, LGTM! @stevesloka or @youngnick it'd be great if you could take a look as well.
I have filed #2943 as a followup issue to add some more documentation on this new feature, it'd be great if we could get this in by mid-next week in order to have it in the upcoming 1.9 release.
stevesloka
left a comment
There was a problem hiding this comment.
Just a small nit, but otherwise lgtm! Thanks for the work here @aberasarte!! 🎉
apis/projectcontour/v1/httpproxy.go
Outdated
| // MaxAge indicates for how long the results of a preflight request can be cached. | ||
| // MaxAge durations are expressed in the Go [Duration format](https://godoc.org/time#ParseDuration). | ||
| // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". | ||
| // Only possitive values are allowed while 0 disables the cache requiring a preflight OPTIONS |
|
Sorry @aberasarte, one more rebase and that nit, and I'll merge this for you. Yay! |
Closes projectcontour#437 Signed-off-by: Aritz Berasarte <raskasso@hotmail.com>
9249b7b to
9d8c3d5
Compare
|
It should be ready for merge now 🤞 . |
|
integration tests flaking on SNI. Merging. |
|
I cannot believe this have been finally merged. Titanic work @aberasarte, thanks for pushing this forward and for being very responsive throughout all the design & impl process. Also thanks to all the reviewers for the time you dedicated here. 👏🏻👏🏻👏🏻 |
Closes #437